package cn.edu.gznu.wecampus.core.web;

import java.io.Serializable;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.stream.Collectors;

import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.web.bind.annotation.GetMapping;

import cn.edu.gznu.wecampus.core.ApplicationContextHolder;
import cn.edu.gznu.wecampus.core.BusinessException;
import cn.edu.gznu.wecampus.core.JpaEntity;
import cn.edu.gznu.wecampus.core.service.CriteriaOperatorService;
import cn.edu.gznu.wecampus.core.utils.PageableUtils;
import cn.edu.gznu.wecampus.core.web.annotation.AbstractCriteriaOperator;
import cn.edu.gznu.wecampus.core.web.annotation.SearchParam;

public interface ISearch<T extends JpaEntity<ID>, ID extends Serializable, SV extends SearchVo<T, ID>>
	extends IRest<T, ID> {
	
	Logger log = LoggerFactory.getLogger(ISearch.class);
	
	@GetMapping(value = "search")
	default RestResponseList search(SV searchVo) {
		int page = searchVo.getPage();
		int size = searchVo.getSize();
		Pageable pageable = PageableUtils.of(page, size);
		Specification<T> specification = this.createSpecification(searchVo);
		return this.findBySpecificationAndPageable(specification, pageable);
	}
	
	default Specification<T> createSpecification(SV searchVo) {
		return new Specification<T>() {
			private static final long serialVersionUID = 2261228345616180943L;

			@Override
			public Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
				try {
					Class<?> clazz = searchVo.getClass();
					List<Field> fields = new LinkedList<Field>();
					while(clazz != null) {
						fields.addAll(
							Arrays.asList(clazz.getDeclaredFields()).stream().filter(field -> {
								SearchParam searchParam = field.getDeclaredAnnotation(SearchParam.class);
								return searchParam != null;
							}).collect(Collectors.toList())
						);
						clazz = clazz.getSuperclass();
					}
					List<Predicate> predicates = new LinkedList<Predicate>();
					for(Field field : fields) {
						field.setAccessible(true);
						Object value = field.get(searchVo);
						if(value == null) {
							continue;
						}
						SearchParam searchParam = field.getDeclaredAnnotation(SearchParam.class);
						String[] searchFields = searchParam.fields();
						CriteriaOperatorService criteriaOperatorService = 
							ApplicationContextHolder.getBean(CriteriaOperatorService.class);
						AbstractCriteriaOperator searchOperator = criteriaOperatorService.getOperator(searchParam.operator());
						Predicate predicate = searchOperator.getPredicate(cb, root, searchFields, value);
						if(searchParam.not()) {
							predicate = cb.not(predicate);
						}
						predicates.add(predicate);
					}
					query.distinct(true).where(
						predicates.toArray(new Predicate[predicates.size()])
					);
					return query.getRestriction();
				} catch(Exception e) {
					throw new BusinessException(e);
				}
			}
		};
	}
	
	default RestResponsePageList findBySpecificationAndPageable(
			Specification<T> specification, Pageable pageable) {
		Page<T> entities = getRepository().findAll(specification, pageable);
		return this.createRestResponsePageList(pageable, entities);
	}
}
